import com.fasterxml.jackson.databind .JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.jsoup.Jsoup;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;


public class BiliBiliCrawler {

    private static String getVideoUrl(String bvid) throws IOException {
    //1. 先获取视频cid
        String cid = getVideoCid(bvid);//2. ⽤于请求 B 站视频播放地址的 API 接⼝地址
        String apiUrl = "https://api.bilibili.com/x/player/playurl"
                        + "?bvid=" + bvid
                        + "&cid=" + cid
                        + "&qn=80" // 清晰度
                        + "&fnval=16" ;// 格式标识+ "&fnver=0" // 格式版本+ "&fourk=1" // 是否允许4K+ "&platform=html5"; // 平台标识
        //3. 添加必要的请求头
        String json = getJson(bvid, apiUrl);//4. 从 JSON 响应中解析出视频的真实播放地址
        return parseVideoUrlFromJson(json);
    }


    // 获取CID
    private static String getVideoCid(String bvid) throws IOException {
        String infoApi = "https://api.bilibili.com/x/web-interface/view?bvid="+ bvid;
        String json = getJson(bvid, infoApi);
        ObjectMapper mapper = new ObjectMapper();JsonNode root = mapper.readTree(json);
        if (root.path("code").asInt() != 0) {
        throw new IOException("获取cid失败: " + root.path("message").asText());
        }

        return root.path("data").path("pages").get(0).path("cid").asText();
    }

    // 添加必要的请求头
    private static String getJson(String bvid, String apiUrl) throws IOException {
        String json = Jsoup.connect(apiUrl).ignoreContentType(true) // 允许接收⾮HTML内容
                .header("Referer", "https://www.bilibili.com/video/" + bvid)// 反爬必须参数
                .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") // 模拟浏览器
                .execute()
                .body();
        return json;
    }

    // 解析URL
    private static String parseVideoUrlFromJson(String json) throws
            IOException {
        ObjectMapper mapper = new ObjectMapper();JsonNode rootNode = mapper.readTree(json);
        if (rootNode.path("code").asInt() != 0) {
            throw new IOException("API请求失败: " +
                    rootNode.path("message").asText());}
        JsonNode durl = rootNode.path("data").path("durl");
        if (durl.isMissingNode() || !durl.isArray() || durl.isEmpty()) {
            throw new IOException("未找到视频地址");
        }
        return durl.get(0).path("url").asText();
    }

    //下载视频
    private static void downloadVideo(String url, String savePath) {
        System.out.println("正在下载视频......");
        try (ReadableByteChannel rbc = Channels.newChannel(new URL(url).openStream());
             FileOutputStream fos = new FileOutputStream(savePath)) {
            fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("下载视频结束......");
    }

    public static void main(String[] args) {
        try {
            // 替换为你要下载的视频BV号
            String bvid = "BV1fZ9FYrECZ";
            String videoUrl = getVideoUrl(bvid);//5.下载 指定URL视频
            downloadVideo(videoUrl, "video.mp4");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}